<?php

/**
 * error class
 * 
 * Error processing
 * 
 * @todo dm9 - any type of warning when dev_mode is set to "on" in set_dev_mode()
 * @todo dm9 - unhighlighted strings
 * @todo dm9 - html in errors is allowed
 * @todo dm9 - don't show line 'trigger_error($message, E_USER_NOTICE);' in backtrace
 * 
 * @todo (!) Ability to see the 1st error in case of fatal error
 * 
 * @author Dmitry Kopytine <dm9@mylinks.ru>
 */
class error
{

	private $dev_mode = false;

	public $error_occured = false;

	/**
	 * Enter description here...
	 *
	 * @var DOMDocument
	 */
	private $dom = null;

	/**
	 * Enter description here...
	 *
	 * @var DOMElement
	 */
	private $errors_node = null;

	/**
	 * Returns an instance
	 *
	 * @return error_core_class
	 */
	private $files = array();

	private $allow_errors = false;

	private $core_path = "";

	private $home_path = "";

	private $xslt_path = "";

	private $encoding;

	public function __construct($core_path, $home_path, $xslt_path, $encoding, $remote_ip = array())
	{
		$this->core_path = $core_path;
		$this->home_path = $home_path;
		$this->xslt_path = $xslt_path;
		
		$this->encoding = $encoding;
		
		error_reporting(E_ALL);
		/**
		 * @todo dm9 - other ini-sets and etc. about errors
		 */
		set_error_handler(array(
			$this, 
			"error_handler" 
		));
		set_exception_handler(array(
			$this, 
			"exception_handler" 
		));
		
		$client_ip4 = $_SERVER["REMOTE_ADDR"];
		$client_ip3 = substr($_SERVER["REMOTE_ADDR"], 0, strrpos($client_ip4, ".", -2) + 1);
		$client_ip2 = substr($_SERVER["REMOTE_ADDR"], 0, strrpos($client_ip3, ".", -2) + 1);
		$client_ip1 = substr($_SERVER["REMOTE_ADDR"], 0, strrpos($client_ip2, ".", -2) + 1);
		
		if (in_array($client_ip4, $remote_ip) || in_array($client_ip3, $remote_ip) || in_array($client_ip2, $remote_ip) || in_array($client_ip1, $remote_ip))
		{
			$this->dev_mode = true;
		}
		
		ini_set('display_errors', $this->dev_mode ? 1 : 0);
	}

	public function set_dev_mode($dev_mode)
	{
		$this->dev_mode = ($dev_mode === true);
	}

	public function is_dev_mode()
	{
		return $this->dev_mode;
	}

	public function allow_errors($allow = true)
	{
		$this->allow_errors = $allow;
	}

	public function trigger_error($message)
	{
		trigger_error($message, E_USER_NOTICE);
	}

	public function exception_handler(Exception $exception)
	{
		/* Yes, error is occured */
		$this->error_occured = true;
		
		/* Checking if XML is created */
		$this->check_if_dom_created();
		
		/* Creating exception node */
		$exception_node = $this->dom->createElement("exception");
		$this->errors_node->appendChild($exception_node);
		
		/* Adding exception description to XML */
		$exception_node->appendChild($this->dom->createElement("desc", $exception->getMessage()));
		$exception_node->appendChild($this->dom->createElement("code", $exception->getCode()));
		
		/* Adding file and line info to XML */
		$this->add_file_info($exception->getFile(), $exception->getLine(), $exception_node);
		
		/* Adding backtrace to XML */
		$trace1 = array();
		$trace1[] = array(
			
			"file" => $exception->getFile(), 
			"line" => $exception->getLine() 
		);
		$trace2 = $exception->getTrace();
		$trace = array_merge($trace1, $trace2);
		$this->add_trace($trace, $exception_node);
		
		/* Because this is the last of executed lines (except of __destrust()s and etc.) */
		echo $this->get_errors_html();
	}

	public function error_handler($err_no, $err_str, $err_file, $err_line)
	{
		/* todo */
		if ($this->allow_errors)
		{
			return;
		}
		
		/* Yes, error is occured */
		$this->error_occured = true;
		
		/* Checking if XML is created */
		$this->check_if_dom_created();
		
		/* Creating error node */
		$error_node = $this->dom->createElement("error");
		$this->errors_node->appendChild($error_node);
		
		/* Adding error description to XML */
		$error_desc = $this->get_error_description($err_no);
		$error_node->appendChild($this->dom->createElement("error_type", $error_desc[0])); // Error type
		$error_node->appendChild($this->dom->createElement("general_desc", $error_desc[1])); // Error type description
		$error_node->appendChild($this->dom->createElement("desc", $err_str)); // Error description
		

		/* Adding file and line info to XML */
		$this->add_file_info($err_file, $err_line, $error_node);
		
		/* Adding backtrace to XML */
		$this->add_trace(debug_backtrace(), $error_node);
	}

	public function is_error_occured()
	{
		return $this->error_occured;
	}

	/**
	 * Returns an XML code
	 *
	 * @return string
	 */
	public function get_errors_html()
	{
		if (is_null($this->dom))
		{
			return "";
		}
		
		//header("Content-type: text/xml; charset=UTF-8");
		//return $this->dom->saveXML();
		

		$xslt = new XSLTProcessor();
		$xslt_dom = new DOMDocument();
		$xslt_dom->load($this->xslt_path);
		$xslt->importStyleSheet($xslt_dom);
		$xhtml = $xslt->transformToXML($this->dom);
		
		//die($this->dom->saveXML());
		

		/**
		 * @todo DileSoft - kill error log
		 */
		//		file_put_contents($this->core_path . "/log/error_" . time() . ".txt", $xhtml);
		return $xhtml;
	}

	protected function check_if_dom_created()
	{
		if (is_null($this->dom))
		{
			$this->dom = new DOMDocument();
			$this->errors_node = $this->dom->createElement("errors");
			$this->errors_node->setAttribute("output_encoding", $this->encoding);
			$this->dom->appendChild($this->errors_node);
			$url_node = $this->dom->createElement("url");
			$url_node_text = $this->dom->createTextNode($this->get_current_url());
			$url_node->appendChild($url_node_text);
			$this->errors_node->appendChild($url_node);
			if (isset($_SERVER["HTTP_REFERER"]))
			{
				$referer_node = $this->dom->createElement("referer");
				$referer_node_text = $this->dom->createTextNode($_SERVER["HTTP_REFERER"]);
				$referer_node->appendChild($referer_node_text);
				$this->errors_node->appendChild($referer_node);
			}
			$paths_node = $this->dom->createElement("paths");
			$path_node = $this->dom->createElement("path", realpath($this->core_path));
			$path_node->setAttribute("type", "CORE");
			$paths_node->appendChild($path_node);
			$path_node = $this->dom->createElement("path", realpath($this->home_path));
			$path_node->setAttribute("type", "HOME");
			$paths_node->appendChild($path_node);
			$this->errors_node->appendChild($paths_node);
		}
	}

	protected function get_current_url()
	{
		$url = strpos($_SERVER["SERVER_PROTOCOL"], "HTTPS") === false ? "http" : "https";
		$url .= "://";
		$url .= $_SERVER["HTTP_HOST"];
		$url .= $_SERVER["REQUEST_URI"];
		return $url;
	}

	protected function add_file_info($err_file, $err_line, $error_node)
	{
		// File info
		$file_path_full = $err_file;
		$file_path_short = $err_file;
		$file_path_type = null;
		$this->cut_filename($file_path_short, $file_path_type);
		$error_node->appendChild($this->dom->createElement("file_path_full", $file_path_full));
		$error_node->appendChild($this->dom->createElement("file_path_short", $file_path_short));
		$error_node->appendChild($this->dom->createElement("file_path_type", $file_path_type));
		// Line info
		$error_node->appendChild($this->dom->createElement("line", $err_line));
	}

	protected function add_trace($trace, $error_node)
	{
		//dd($trace);
		$trace_node = $this->dom->createElement("backtrace");
		$error_node->appendChild($trace_node);
		foreach ($trace as $idx => $val)
		{
			//if ($idx == 0) continue;
			$t_function = (isset($val["function"])) ? ($val["function"] . "()") : "";
			$t_line = (isset($val["line"])) ? $val["line"] : "";
			$t_file = (isset($val["file"])) ? $val["file"] : "";
			$t_class = (isset($val["class"])) ? $val["class"] : "";
			$t_type = (isset($val["type"])) ? $val["type"] : "";
			if ($t_file == "" or $t_line == "")
			{
				continue;
			}
			$t_call = "";
			if ($t_class != "" and $t_function != "")
			{
				$t_call = $t_class . $t_type . $t_function;
			}
			elseif ($t_function != "")
			{
				$t_call = $t_function;
			}
			$file_path_full = $t_file;
			$file_path_short = $t_file;
			$file_path_type = null;
			$this->cut_filename($file_path_short, $file_path_type);
			$trace_item_node = $this->dom->createElement("item");
			$trace_node->appendChild($trace_item_node);
			$trace_item_node->appendChild($this->dom->createElement("call", $t_call));
			$trace_item_node->appendChild($this->dom->createElement("class", $t_class));
			$trace_item_node->appendChild($this->dom->createElement("type", $t_type));
			$trace_item_node->appendChild($this->dom->createElement("function", $t_function));
			$trace_item_node->appendChild($this->dom->createElement("file_path_full", $file_path_full));
			$trace_item_node->appendChild($this->dom->createElement("file_path_short", $file_path_short));
			$trace_item_node->appendChild($this->dom->createElement("file_path_type", $file_path_type));
			$trace_item_node->appendChild($this->dom->createElement("line", $t_line));
			
			$file_lines = file($t_file);
			if (isset($file_lines[$t_line - 1]))
			{
				$file_line = $this->highlight_string($file_lines[$t_line - 1], $t_function, $t_class, $t_type);
			}
			else
			{
				/**
				 * That was a strange bug - PHP couldn't find a line of file, which was 
				 */
				$file_line = "Line #{$t_line} of the file {$t_file} was not found";
			}
			$trace_item_node->appendChild($this->dom->createElement("line_text", $file_line));
		}
	}

	protected function get_error_description($err_no)
	{
		switch ($err_no)
		{
			case E_ERROR:
				{
					return array(
						
						"E_ERROR", 
						"Fatal run-time errors. These indicate errors that can not be recovered from, such as a memory allocation problem. Execution of the script is halted." 
					);
					break;
				}
			case E_WARNING:
				{
					return array(
						
						"E_WARNING", 
						"Run-time warnings (non-fatal errors). Execution of the script is not halted." 
					);
					break;
				}
			case E_PARSE:
				{
					return array(
						
						"E_PARSE", 
						"Compile-time parse errors. Parse errors should only be generated by the parser." 
					);
					break;
				}
			case E_NOTICE:
				{
					return array(
						
						"E_NOTICE", 
						"Run-time notices. Indicate that the script encountered something that could indicate an error, but could also happen in the normal course of running a script." 
					);
					break;
				}
			case E_CORE_ERROR:
				{
					return array(
						
						"E_CORE_ERROR", 
						"Fatal errors that occur during PHP's initial startup. This is like an E_ERROR, except it is generated by the core of PHP." 
					);
					break;
				}
			case E_CORE_WARNING:
				{
					return array(
						
						"E_CORE_WARNING", 
						"Warnings (non-fatal errors) that occur during PHP's initial startup. This is like an E_WARNING, except it is generated by the core of PHP." 
					);
					break;
				}
			case E_COMPILE_ERROR:
				{
					return array(
						
						"E_COMPILE_ERROR", 
						"Fatal compile-time errors. This is like an E_ERROR, except it is generated by the Zend Scripting Engine." 
					);
					break;
				}
			case E_COMPILE_WARNING:
				{
					return array(
						
						"E_COMPILE_WARNING", 
						"Compile-time warnings (non-fatal errors). This is like an E_WARNING, except it is generated by the Zend Scripting Engine." 
					);
					break;
				}
			case E_USER_ERROR:
				{
					return array(
						
						"E_USER_ERROR", 
						"User-generated error message. This is like an E_ERROR, except it is generated in PHP code by using the PHP function trigger_error()." 
					);
					break;
				}
			case E_USER_WARNING:
				{
					return array(
						
						"E_USER_WARNING", 
						"User-generated warning message. This is like an E_WARNING, except it is generated in PHP code by using the PHP function trigger_error()." 
					);
					break;
				}
			case E_USER_NOTICE:
				{
					return array(
						
						"E_USER_NOTICE", 
						"User-generated notice message. This is like an E_NOTICE, except it is generated in PHP code by using the PHP function trigger_error()." 
					);
					break;
				}
			case E_STRICT:
				{
					return array(
						
						"E_STRICT", 
						"Run-time notices. Enable to have PHP suggest changes to your code which will ensure the best interoperability and forward compatibility of your code." 
					);
					break;
				}
			default:
				{
					return array(
						
						"", 
						"Unknown error" 
					);
				}
		}
	}

	protected function file_as_array($file_name)
	{
		if (isset($this->files[$file_name]))
		{
			return $this->files[$file_name];
		}
		else
		{
			return $this->files[$file_name] = file($file_name);
		}
	}

	protected function cut_filename(&$file_name, &$type)
	{
		$file_name = realpath($file_name);
		$core_path = realpath($this->core_path);
		$home_path = realpath($this->home_path);
		if (strncmp($file_name, $home_path, strlen($home_path)) == 0)
		{
			$file_name = substr($file_name, strlen($home_path) + 1);
			$type = "HOME";
		}
		elseif (strncmp($file_name, $core_path, strlen($core_path)) == 0)
		{
			$file_name = substr($file_name, strlen($core_path) + 1);
			$type = "CORE";
		}
		else
		{
			$type = "";
		}
	}

	protected function highlight_string($str, $func_name, $class_name, $type_name)
	{
		/**
		 * @todo dm9 - UTF-8 - config ?
		 */
		$str = htmlspecialchars($str, ENT_NOQUOTES, "UTF-8");
		$func_name = str_replace("()", "", $func_name);
		$func_name_quoted = preg_quote($func_name, "/");
		$class_name_quoted = preg_quote($class_name, "/");
		$variable_regexp = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*';
		if ($func_name == "__construct")
		{
			$str = preg_replace("/(new\s+{$class_name_quoted})(\s*\()/i", "<b>\\1</b>\\2", $str);
		}
		elseif ($type_name == "::")
		{
			$current_class_name = $class_name;
			$match = false;
			$parent_class = null;
			do
			{
				$current_class = new ReflectionClass($current_class_name);
				/* var $parent_class ReflectionClass */
				$parent_class = $current_class->getParentClass();
				$current_class_name_quoted = preg_quote($current_class_name);
				/**
				 * @todo dm9 -
				 * Really, self is neccessary - 'cause class name might be substituted with it.
				 * Parent lookup - don't know. I wrote it due to mistake and now I pity to delete it.
				 * Then - we need child look-down :) That is, if class of this string is a child of
				 * $class_name and it doesn't have got a method $func_name - then it's our class...
				 */
				$regexp = "/(({$current_class_name_quoted}|self)::\s*{$func_name_quoted})(\s*\()/i";
				$match = preg_match($regexp, $str);
				if ($parent_class)
				{
					$current_class_name = $parent_class->getName();
				}
			}
			while ($parent_class and !$match);
			if ($match)
			{
				$str = preg_replace($regexp, "<b>\\1</b>\\3", $str);
			}
		}
		elseif ($type_name == "->")
		{
			$regexp = "/((\\\${$variable_regexp})\s*-&gt;\s*{$func_name_quoted})(\s*\()/i";
			$ret = preg_match_all($regexp, $str, $matches, PREG_PATTERN_ORDER);
			$str = preg_replace($regexp, "<b>\\1</b>\\3", $str);
			if ($ret)
			{
				$str .= " // " . join(", ", $matches[2]) . ($ret > 1 ? " are " : " is ") . $class_name . ($ret > 1 ? " classes" : " class");
			}
		}
		else
		{
			$str = preg_replace("/({$func_name_quoted})(\s*\()/i", "<b>\\1</b>\\2", $str);
		}
		$str = htmlspecialchars($str, ENT_NOQUOTES, "UTF-8");
		return $str;
	}
	
	public function clear_errors()
	{
		$this->error_occured = false;
		$this->dom = null;
	}

}

?>